﻿// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable StringLiteralTypo
// ReSharper disable UnusedMember.Local

/* AvaloniaBindingGenerator.cs -- генератор компайл-тайм привязок Avalonia
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

#endregion

namespace AM.SourceGeneration
{
    /// <summary>
    /// Генератор компайл-тайм привязок Avalonia.
    /// <example>
    /// <code>
    /// partial class BindingDemo
    /// {
    ///     // Некое свойство, нуждающееся в привязке.
    ///     [CreateBinding]
    ///     public string? Title { get; set; }
    /// }
    ///
    /// var model = DataContext = new BindingDemo();
    /// var textBox = new TextBox
    /// {
    ///     [TextBox.TextProperty] = model.TitleBinding()
    /// };
    /// </code>
    /// </example>
    /// </summary>
    [Generator]
    public sealed class CreateBindingGenerator
        : IIncrementalGenerator
    {
        #region Constants

        internal const string AttributeName = "AM.Avalonia.SourceGeneration.CreateBindingAttribute";

        #endregion

        #region Private members

        private static bool IsSyntaxTargetForGeneration (SyntaxNode node)
            => node is PropertyDeclarationSyntax m && m.AttributeLists.Count > 0;

        private static List<IPropertySymbol> GetSemanticTargetForGeneration
            (
                GeneratorSyntaxContext context
            )
        {
            var result = new List<IPropertySymbol>();
            var propertyDeclaration = (PropertyDeclarationSyntax) context.Node;

            foreach (var attributeList in propertyDeclaration.AttributeLists)
            {
                foreach (var attribute in attributeList.Attributes)
                {
                    if (!(context.SemanticModel.GetSymbolInfo (attribute).Symbol is IMethodSymbol symbol))
                    {
                        continue;
                    }

                    var type = symbol.ContainingType;
                    var fullName = type.ToDisplayString();
                    if (fullName == AttributeName)
                    {
                        if (context.SemanticModel.GetDeclaredSymbol (propertyDeclaration) is IPropertySymbol propertySymbol)
                        {
                            result.Add (propertySymbol);
                        }
                    }
                }
            }

            return result;
        }

        private static void Execute
            (
                IEnumerable<IPropertySymbol> collected,
                SourceProductionContext context
            )
        {
            var types = collected.GroupBy<IPropertySymbol, INamedTypeSymbol>
                (
                    it => it.ContainingType, SymbolEqualityComparer.Default
                );

            foreach (var group in types)
            {
                var classSource = ProcessClass (group.Key, group.ToList());
                if (!string.IsNullOrEmpty (classSource))
                {
                    context.AddSource
                        (
                            $"{group.Key.Name}_bindings.g.cs",
                            SourceText.From (classSource!, Encoding.UTF8)
                        );
                }
            }
        }

        private static string? ProcessClass
            (
                INamedTypeSymbol classSymbol,
                IList<IPropertySymbol> properties
            )
        {
            if (!classSymbol.ContainingSymbol.Equals
                    (
                        classSymbol.ContainingNamespace,
                        SymbolEqualityComparer.Default
                    ))
            {
                // оказались вне пространства имен, это странно
                return null;
            }

            var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();

            var source = new StringBuilder (
                $@"// <auto-generated />

namespace {namespaceName}
{{
    partial class {classSymbol.Name}
    {{
");

            foreach (var propertySymbol in properties)
            {
                ProcessProperty (classSymbol, source, propertySymbol);
            }

            source.Append ("} }");
            return source.ToString();
        }

        private static void ProcessProperty
            (
                INamedTypeSymbol classSymbol,
                StringBuilder source,
                IPropertySymbol propertySymbol
            )
        {
            var className = classSymbol.Name;
            var propertyName = propertySymbol.Name;
            var propertyType = propertySymbol.Type.GetTypeName();

            source.Append
                (
                    $@"

                    public Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension {propertyName}Binding()
                    {{
                        return AM.Avalonia.AvaloniaUtility.MakeBinding<{propertyType}>
                            (
                                ""{propertyName}"",
                                it => (({className}) it).{propertyName},
                                (it, value) => (({className}) it).{propertyName} = ({propertyType}) value
                            );
                    }}
"
                );
        }

        #endregion

        #region IIncrementalGenerator members

        /// <inheritdoc cref="IIncrementalGenerator.Initialize"/>
        public void Initialize
            (
                IncrementalGeneratorInitializationContext context
            )
        {
            // отфильтровываем нужные поля
            var declarations = context.SyntaxProvider.CreateSyntaxProvider
                (
                    predicate: (s, _) => IsSyntaxTargetForGeneration (s),
                    transform: (ctx, _) => GetSemanticTargetForGeneration (ctx)
                );

            // объединяем данные вместе
            var compilation = context.CompilationProvider.Combine (declarations.Collect());

            // регистрируем метод, который занимается собственно генерацией
            context.RegisterSourceOutput
                (
                    compilation,
                    (spc, source) =>
                    {
                        var collected = source.Item2.SelectMany (it => it).ToList();
                        Execute (collected, spc);
                    });
        }

        #endregion
    }
}
